
#if defined(WIN32) || defined(WIN64)
#pragma unmanaged
#endif

#include <Liberty3D/Library/AssetDepotFolder.h>
#include <Liberty3D/Library/IAssetDepotListener.h>
#include <Liberty3D/Library/Task.h>
#include <Liberty3D/Library/TaskScheduler.h>
#include <Liberty3D/Engine/Conversion.h>
#include <Liberty3D/Engine/ISerializer.h>
#include <Liberty3D/Engine/Path.h>
#include <Liberty3D/Engine/PosixException.h>

#include <Liberty3D/Engine/XML/Parser.h>
#include <Liberty3D/Engine/XML/INode.h>

#include <algorithm>
#include <streambuf>
#include <fstream>
#include <sstream>

#ifndef WIN32
#include <dirent.h>
#include <errno.h>
#include <sys/stat.h>
#include <thread>
#else
#include <windows.h>
#include <Liberty3D/Engine/Win32Exception.h>
#endif

using namespace Liberty::Engine;
using namespace Liberty::Engine::Xml;
using namespace Liberty::Library;

AssetDepotFolderDetectState::AssetDepotFolderDetectState() : metaFileMissing(false) {
}

void AssetDepotFolder::detectChanges() {
    // Make sure we aren't scanning the repo in two thread simulaneously.
    while (_detectChangesRunning) {
#ifdef WIN32
        Sleep(5);
#else
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
#endif
    }

    _detectChangesRunning = true;

    AssetDepotFolderDetectState state;

    try {
        state.lock = std::unique_ptr<ISharedLock>(_entryMutex->lockShared());
        state.pendings.push_back(_root);

        while (state.pendings.size() > 0) {
            state.folder = *(state.pendings.begin());

            state.pendings.erase(state.pendings.begin());
            detectChanges(state);
        }
    }
    catch (...) {
        _detectChangesRunning = false;
        throw;
    }

    _detectChangesRunning = false;

    if (state.metaFileMissing.size() > 0) {
        TaskScheduler::scheduleMethod1<void, AssetDepotFolder, std::list<Uuid>, const std::list<Uuid>&>(
            shared_from_this(),
            &AssetDepotFolder::generateMetaFiles,
            state.metaFileMissing);
    }

    if ((state.added.size()    == 0) &&
        (state.modified.size() == 0) &&
        (state.moved.size()    == 0) &&
        (state.unfound.size()  == 0) &&
        (state.uuids.size()    == 0)) {
        // Nothing changes...
        return;
    }

    {
        std::unique_ptr<IExclusiveLock> guard(state.lock->upgrade());

        // Increment the revision of the depot.
        _revision++;
        dirtyLibraryLocked();

        // Rebuild the uuid -> AssetDepotEntry map.
        _map.clear();
        buildMap(_root);
    }

    state.lock.reset();

    {
        // Erase the cached objects version of assets that were tagged 'unfound'.
        AssetDepotEntryList::const_iterator sit, send(state.unfound.end());

        for (sit = state.unfound.begin(); sit != send; ++sit) {
            const std::shared_ptr<AssetDepotEntry>& entry(*sit);

            _importerCache.clear(entry->uuid);
        }
    }

    // Notify all the listeners
    if (_listeners.size() > 0) {
        std::string                               projectPath;
        AssetDepotListenerVector::const_iterator  lit, lend(_listeners.end());
        
        {
            AssetDepotEntryList::const_iterator sit, send(state.unfound.end());

            for (sit = state.unfound.begin(); sit != send; ++sit) {
                projectPath = (*sit)->projectPath();
            
                for (lit = _listeners.begin(); lit != lend; ++lit) {
                    (*lit)->assetRemoved(projectPath);
                }
            }
        
            for (sit = state.modified.begin(), send = state.modified.end(); sit != send; ++sit) {
                projectPath = (*sit)->projectPath();
            
                for (lit = _listeners.begin(); lit != lend; ++lit) {
                    (*lit)->assetModified(projectPath);
                }
            }
        
            for (sit = state.added.begin(), send = state.added.end(); sit != send; ++sit) {
                projectPath = (*sit)->projectPath();
            
                for (lit = _listeners.begin(); lit != lend; ++lit) {
                    (*lit)->assetAdded(projectPath);
                }
            }
        }

        {
            AssetDepotFolderDetectState::AssetDepotEntryMovedList::const_iterator mit, mend(state.moved.end());

            for (mit = state.moved.begin(), mend = state.moved.end(); mit != mend; ++mit) {
                projectPath = (*mit).first->projectPath();
            
                for (lit = _listeners.begin(); lit != lend; ++lit) {
                    (*lit)->assetMoved((*mit).second, projectPath);
                }
            }
        }

        {
            AssetDepotFolderDetectState::AssetDepotEntryDuplicateList::const_iterator mit, mend(state.uuids.end());

            for (mit = state.uuids.begin(), mend = state.uuids.end(); mit != mend; ++mit) {
                projectPath = (*mit).first->projectPath();
            
                for (lit = _listeners.begin(); lit != lend; ++lit) {
                    (*lit)->assetUuidDuplicated(projectPath, (*mit).second, (*mit).first->uuid);
                }
            }
        }
    }
}

bool AssetDepotFolder::shouldIgnoreFile(const char* name, bool excludeMeta) const {
    size_t l = strlen(name);
    
    if (excludeMeta && (l >= 5)) {
        if (strcmp(name + (l - 5), ".meta") == 0) {
            return true;
        }
    }
    
    return
    (strcmp(name, ".")              == 0) ||
    (strcmp(name, "..")             == 0) ||
    (strcmp(name, ".DS_Store")      == 0) ||
    (strcmp(name, ".git")           == 0) ||
    (strcmp(name, ".gitattributes") == 0) ||
    (strcmp(name, ".svn")           == 0) ||
    (strcmp(name, "CVS")            == 0) ||
    (strncmp(name, ".liberty.", 9)  == 0);
}

bool AssetDepotFolder::shouldCreateMetaFile(const std::string& name) const {
    const char* p = name.c_str();

    while (*p == '/') {
        p++;
    }

    size_t l = strlen(p);
    
    if (l >= 5) {
        if (strcmp(p + (l - 5), ".meta") == 0) {
            return false;
        }
    }

    if (l < 8) {
        return false;
    }

    return strncmp(p, "Assets/", 7) == 0;
}

bool AssetDepotFolder::shouldIgnorePath(const std::string& name) const {
    return false;
}

void AssetDepotFolder::recursiveRemove(AssetDepotEntryList& unfound, const std::shared_ptr<AssetDepotEntryFolder>& folder) {
    AssetDepotEntryFolder::EntryList::const_iterator it, end(folder->childs.end());
    
    for (it = folder->childs.begin(); it != end; ++it) {
        std::shared_ptr<AssetDepotEntryFolder> subFolder(std::dynamic_pointer_cast<AssetDepotEntryFolder>(*it));
        
        if (subFolder) {
            recursiveRemove(unfound, folder);
        }
        
        unfound.push_back(*it);
    }
}

static void removeFromNameList(std::list<std::string>& names, const std::string& d_name) {
    std::list<std::string>::iterator nameIt(std::find(names.begin(), names.end(), d_name));

    if (nameIt != names.end()) {
        names.erase(nameIt);
    }
}

#ifdef WIN32
static void findEntry(const std::list<WIN32_FIND_DATA>& entries, const std::string& name, WIN32_FIND_DATA& entry) {
    std::list<WIN32_FIND_DATA>::const_iterator it, end(entries.end());

    std::wstring nameW = UTF8_To_UTF16(name);

    for (it = entries.begin(); it != end; ++it) {
        if (wcscmp(nameW.c_str(), (*it).cFileName) == 0) {
            entry = *it;
            return;
        }
    }

    memset(&entry, 0, sizeof(entry));
}

static unsigned int hashEntry(WIN32_FIND_DATA& entry) {
    if (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
        return 0;
    }

    return entry.nFileSizeLow ^ entry.nFileSizeHigh ^ entry.ftLastWriteTime.dwLowDateTime ^ entry.ftLastWriteTime.dwHighDateTime;
}

unsigned int AssetDepotFolder::hashFile(const std::string& fileName) {
    std::wstring fileNameW(UTF8_To_UTF16(absolutePath(fileName)));

    DWORD dwFlags = GetFileAttributes(fileNameW.c_str());

    if (dwFlags == INVALID_FILE_ATTRIBUTES) {
        throw Win32Exception(GetLastError());
    }

    if (dwFlags & FILE_ATTRIBUTE_DIRECTORY) {
        return 0;
    }

    HANDLE hFile = CreateFile(fileNameW.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

    if (hFile != INVALID_HANDLE_VALUE) {
        FILETIME fileTime;
        DWORD    sizeLow, sizeHigh;

        GetFileTime(hFile, NULL, NULL, &fileTime);
        sizeLow = GetFileSize(hFile, &sizeHigh);
        CloseHandle(hFile);

        return sizeLow ^ sizeHigh ^ fileTime.dwLowDateTime ^ fileTime.dwHighDateTime;
    }
    else {
        throw Win32Exception(GetLastError());
    }
}

#else
static unsigned int hashEntry(struct stat& s) {
    if (s.st_mode & DT_DIR) {
        return 0;
    }

    return (unsigned int)(s.st_size ^ s.st_mtimespec.tv_sec);
}

unsigned int AssetDepotFolder::hashFile(const std::string& fileName) {
    struct stat s;

    if (stat(absolutePath(fileName).c_str(), &s) != 0) {
        throw PosixException(errno);
    }

    return hashEntry(s);
}

#endif

/* This function assume that you already have an exclusive lock on the depot's mutex. */
void AssetDepotFolder::updateUuidForPath(const std::shared_ptr<AssetDepotEntry>& entry) {
    try {
        std::string metaPath(entry->projectPath() + ".meta");
        std::string metaPathAbs(absolutePath(metaPath));

        std::shared_ptr<INode> doc (Parser::parse(metaPathAbs));
        std::shared_ptr<INode> node(doc);

        if (node) {
            node = node->firstChildElement(std::string()); // get objects
        }

        if (node) {
            node = node->firstChildElement(std::string()); // get object
        }

        if (node) {
            node = node->firstChildElement("uuid"); // get uuid
        }

        if (node) {
            std::shared_ptr<INode> uuidText; // get text

            while ((uuidText = node->firstChild())) {
                node->removeChild(uuidText);
            }

            node->addText(entry->uuid.toString());

            doc->toXml(metaPathAbs.c_str());
            entry->hashMeta = hashFile(metaPath);
        }
    }
    catch (...) {
    }
}

Uuid AssetDepotFolder::createUuidForPath(const std::string& name) const {
    try {
        std::shared_ptr<INode> node(Parser::parse(absolutePath(name + ".meta")));

        if (node) {
            node = node->firstChildElement(std::string()); // get objects
        }

        if (node) {
            node = node->firstChildElement(std::string()); // get object
        }

        if (node) {
            node = node->firstChildElement("uuid"); // get uuid
        }

        if (node) {
            node = node->firstChild(); // get text
        }

        if (node) {
            Uuid uuid(node->toString());

            if (!uuid.isNull()) {
                return uuid;
            }
        }
    }
    catch (...) {
    }

    return Uuid::create();
}

std::shared_ptr<AssetDepotEntryFolder> AssetDepotFolder::changeFileToFolder(AssetDepotFolderDetectState& state, const std::shared_ptr<AssetDepotEntryFile>& subFile) {
    std::shared_ptr<AssetDepotEntryFolder> subFolder(new AssetDepotEntryFolder());

    subFolder->uuid   = createUuidForPath(state.name);
    subFolder->parent = state.folder;
    subFolder->name   = subFile->name;

    {
        std::unique_ptr<IExclusiveLock> lock(state.lock->upgrade());

        state.folder->remove(subFile);
        state.folder->add(subFolder);
    }

    state.modified.push_back(subFolder);
    return subFolder;
}

/* This function assume that you already have an exclusive lock on the depot's mutex. */
void AssetDepotFolder::duplicateId(AssetDepotFolderDetectState* state, const std::shared_ptr<AssetDepotEntry>& entry) {
    Uuid oldEntry(entry->uuid);

    entry->uuid = Uuid::create();

    updateUuidForPath(entry);
    dirtyLibraryLocked();

    if (state) {
        state->uuids.push_back(std::pair<std::shared_ptr<AssetDepotEntry>, Uuid>(entry, oldEntry));
    }
    else {
        std::string projectPath(entry->projectPath());

        AssetDepotListenerVector::const_iterator  lit, lend(_listeners.end());

        for (lit = _listeners.begin(); lit != lend; ++lit) {
            (*lit)->assetUuidDuplicated(projectPath, oldEntry, entry->uuid);
        }
    }
}

void AssetDepotFolder::createEntry(AssetDepotFolderDetectState& state, const std::shared_ptr<AssetDepotEntry>& entry) {
    AssetDepotEntryMap::iterator     it(_map.find(entry->uuid));
    std::shared_ptr<AssetDepotEntry> otherEntry;

    if (it != _map.end()) {
        otherEntry = (*it).second;
    }

    if (!otherEntry) {
        AssetDepotFolderDetectState::AssetDepotEntryList::iterator it, end(state.unfound.end());

        for (it = state.unfound.begin(); it != end; ++it) {
            if ((*it)->uuid == entry->uuid) {
                otherEntry = *it;
                state.unfound.erase(it);
                break;
            }
        }
    }

    std::unique_ptr<IExclusiveLock> lock(state.lock->upgrade());

    if (otherEntry) {
        std::string otherPath(otherEntry->projectPath());

        if (!exists(otherPath)) {
            // Moved.
            std::shared_ptr<AssetDepotEntryFolder> parent(otherEntry->parent.lock());

            if (parent) {
                parent->remove(otherEntry);
            }

            otherEntry->parent = state.folder;
            state.folder->add(otherEntry);
            state.moved.push_back(std::pair<std::shared_ptr<AssetDepotEntry>, std::string>(entry, otherPath));
        }
        else {
            // Duplicate...
            duplicateId(&state, entry);
            state.folder->add(entry);
            state.added.push_back(entry);
        }
    }
    else {
        state.folder->add(entry);
        state.added.push_back(entry);
    }
}

std::shared_ptr<AssetDepotEntryFolder> AssetDepotFolder::createFolder(AssetDepotFolderDetectState& state) {
    std::shared_ptr<AssetDepotEntryFolder> subFolder(new AssetDepotEntryFolder());

    subFolder->uuid   = createUuidForPath(state.name);
    subFolder->parent = state.folder;
    subFolder->name   = state.shortName;

    createEntry(state, subFolder);
    return subFolder;
}

std::shared_ptr<AssetDepotEntryFile> AssetDepotFolder::changeFolderToFile(AssetDepotFolderDetectState& state, const std::shared_ptr<AssetDepotEntryFolder>& subFolder) {
    recursiveRemove(state.unfound, subFolder);

    std::shared_ptr<AssetDepotEntryFile> subFile(new AssetDepotEntryFile());

    subFile->uuid   = createUuidForPath(state.name);
    subFile->parent = state.folder;
    subFile->name   = subFolder->name;

    {
        std::unique_ptr<IExclusiveLock> lock(state.lock->upgrade());

        state.folder->remove(subFolder);
        state.folder->add(subFile);
    }

    state.modified.push_back(subFile);
    return subFile;
}

std::shared_ptr<AssetDepotEntryFile> AssetDepotFolder::createFile(AssetDepotFolderDetectState& state) {
    std::shared_ptr<AssetDepotEntryFile> subFile(new AssetDepotEntryFile());

    subFile->uuid   = createUuidForPath(state.name);
    subFile->parent = state.folder;
    subFile->name   = state.shortName;

    createEntry(state, subFile);
    return subFile;
}

void AssetDepotFolder::metadataChanged(AssetDepotFolderDetectState& state, const std::shared_ptr<AssetDepotEntry>& entry, unsigned int hashMeta) {
    Uuid uuid(createUuidForPath(state.name));

    if (uuid != entry->uuid) {
        {
            std::unique_ptr<IExclusiveLock> lock(state.lock->upgrade());

            // Uuid changed...
            entry->uuid     = uuid;
            entry->hashMeta = hashMeta;

            std::shared_ptr<AssetDepotEntryFile> file(std::dynamic_pointer_cast<AssetDepotEntryFile>(entry));

            if (file) {
                file->revision++;
            }
        }

        state.modified.push_back(entry);
    }
}

void AssetDepotFolder::detectChanges(AssetDepotFolderDetectState& state, bool folder, unsigned int hash, unsigned int hashMeta) {
    std::shared_ptr<AssetDepotEntry>       subEntry (state.folder->find(state.shortName));
    std::shared_ptr<AssetDepotEntryFolder> subFolder(std::dynamic_pointer_cast<AssetDepotEntryFolder>(subEntry));
    std::shared_ptr<AssetDepotEntryFile>   subFile  (std::dynamic_pointer_cast<AssetDepotEntryFile>  (subEntry));

    if (folder) {
        if (!subFolder && subFile) {
            subEntry = subFolder = changeFileToFolder(state, subFile);
            subFile  = std::shared_ptr<AssetDepotEntryFile>();
        }
        else if (!subFolder) {
            subEntry = subFolder = createFolder(state);
        }
        else {
            if (subFolder->hashMeta != hashMeta) {
                metadataChanged(state, subFolder, hashMeta);

                {
                    std::unique_ptr<IExclusiveLock> lock(state.lock->upgrade());

                    if (subFile->hashMeta != hashMeta) {
                        subFile->hashMeta = hashMeta;
                        subFile->revisionMeta++;
                    }
                }

                state.modified.push_back(subFolder);
            }
        }

        state.pendings.push_back(subFolder);
    }
    else {
        if (subFolder) {
            // Change type... Was a folder, became a file...
            subEntry  = subFile = changeFolderToFile(state, subFolder);
            subFolder = std::shared_ptr<AssetDepotEntryFolder>();
        }
        else if (!subFile) {
            subEntry = subFile = createFile(state);
        }
        else {
            if ((subFile->hashMeta != hashMeta) ||
                (subFile->hash     != hash)) {
                if (subFile->hashMeta != hashMeta) {
                    metadataChanged(state, subFile, hashMeta);
                }

                {
                    std::unique_ptr<IExclusiveLock> lock(state.lock->upgrade());

                    if (subFile->hash != hash) {
                        subFile->hash     = hash;
                        subFile->revision++;
                    }

                    if (subFile->hashMeta != hashMeta) {
                        subFile->hashMeta = hashMeta;
                        subFile->revisionMeta++;
                    }
                }

                state.modified.push_back(subFile);
            }
        }
    }

    if (hashMeta == 0) {
        if (shouldCreateMetaFile(state.name)) {
            state.metaFileMissing.push_back(subEntry->uuid);
        }
    }
}

void AssetDepotFolder::detectChanges(AssetDepotFolderDetectState& state) {
    std::string folderProjectPath(state.folder->projectPath());
    std::string folderAbsolutePath(absolutePath(folderProjectPath));

    std::list<std::string> names = state.folder->names();

#ifdef WIN32
    std::list<WIN32_FIND_DATA> entries;
    WIN32_FIND_DATA            entry;
    WIN32_FIND_DATA            entryMeta;
    HANDLE                     d;

    std::string folderAbsolutePathWin(Path::append(folderAbsolutePath, "*.*"));

    Path::winifyPath(folderAbsolutePathWin);
    d = FindFirstFile(UTF8_To_UTF16(folderAbsolutePathWin).c_str(), &entry);

    if (d != INVALID_HANDLE_VALUE) {
        do {
            std::string cFileName(UTF16_To_UTF8(entry.cFileName));

            if (shouldIgnoreFile(cFileName.c_str(), false)) {
                continue;
            }

            entries.push_back(entry);
        } while (FindNextFile(d, &entry));

        FindClose(d);

        std::list<WIN32_FIND_DATA>::iterator it, end(entries.end());

        for (it = entries.begin(); it != end; ++it) {
            entry = *it;
            state.shortName = UTF16_To_UTF8(entry.cFileName);

            if (shouldIgnoreFile(state.shortName.c_str(), true)) {
                continue;
            }

            state.name = Path::append(folderProjectPath, state.shortName);

            if (shouldIgnorePath(state.name)) {
                continue;
            }

            removeFromNameList(names, state.shortName);

            findEntry(entries, state.shortName + ".meta", entryMeta);

            int hash     = hashEntry(entry);
            int hashMeta = hashEntry(entryMeta);

            detectChanges(state, entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY? true: false, hash, hashMeta);
        }
    }
#else
    DIR* d = opendir(folderAbsolutePath.c_str());

    if (d) {
        struct dirent* entry;
        
        while ((entry = readdir(d)) != nullptr) {
            if (shouldIgnoreFile(entry->d_name)) {
                continue;
            }
            
            state.shortName = entry->d_name;
            state.name = Path::append(folderProjectPath, state.shortName);
            
            if (shouldIgnorePath(state.name)) {
                continue;
            }
            
            removeFromNameList(names, entry->d_name);
            
            struct stat s;
            struct stat sMeta;
            
            if (stat(absolutePath(state.name).c_str(), &s) != 0) {
                throw PosixException(errno);
            }

            if (stat(absolutePath(state.name + ".meta").c_str(), &sMeta) != 0) {
                memset(&sMeta, 0, sizeof(sMeta));
            }
            
            int hash     = hashEntry(s);
            int hashMeta = hashEntry(sMeta);

            detectChanges(state, entry->d_type & DT_DIR? true: false, hash, hashMeta);
        }

        closedir(d);
    }
#endif
    
    if (names.size() > 0) {
        std::list<std::string>::iterator it, end(names.end());
        
        for (it = names.begin(); it != end; ++it) {
            std::shared_ptr<AssetDepotEntry> entry(state.folder->find(*it));

            if (entry) {
                state.unfound.push_back(entry);

                std::unique_ptr<IExclusiveLock> lock(state.lock->upgrade());

                state.folder->remove(entry);
            }
        }
    }
}

void AssetDepotFolder::dirtyLibraryLocked() {
    _libraryDirty = true;

    if (!_libraryDirtyTask) {
        _libraryDirtyTask = TaskScheduler::scheduleMethod<void, AssetDepotFolder>(shared_from_this(), &AssetDepotFolder::save);
    }
}

void AssetDepotFolder::generateMetaFiles(const std::list<Uuid>& uuids) {
    auto it (uuids.begin());
    auto end(uuids.end  ());

    for (; it != end; ++it) {
        // This call will create a AssetImporter object. If the .meta file still doesn't exist,
        // it will get automatically added to the dirty object lists.
        importer(*it);
    }
}

void AssetDepotFolder::updateFile(const std::shared_ptr<AssetDepotEntryFile>& entry) {
    std::unique_ptr<IExclusiveLock> guard(_entryMutex->lock());

    entry->hash = hashFile(entry->projectPath());
    dirtyLibraryLocked();
}

void AssetDepotFolder::updateMeta(const std::shared_ptr<AssetDepotEntry>& entry) {
    std::unique_ptr<IExclusiveLock> guard(_entryMutex->lock());

    entry->hashMeta = hashFile(entry->projectPath() + ".meta");
    dirtyLibraryLocked();
}
